package ferp.core.game;

import com.google.gson2.Gson;
import ferp.core.ai.strategy.Strategy;
import ferp.core.card.Card;
import ferp.core.card.Deck;
import ferp.core.log.Log;
import ferp.core.mode.Mode;
import ferp.core.player.*;
import ferp.core.state.State;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

/**
 * User: igorgok
 * Date: 5/19/11 2:34 PM
 */
public class Game
{
  public static void log(Game g, String event)
  {
    if (g != null)
      g.log.add(event);
  }

  // constants
  public static final int PLAYERS = 3;
  public static final int TALON_SIZE = 2;
  public static final int MAXIMUM_TRICKS = 10;
  public static final int MAXIMUM_ROUNDS = 10;
  public static final int HAND_SIZE = 10;

  public static final int NO_ID = -1;

  // JSON serializer
  public static final Gson gson = new Gson();

  public static final class Error extends Exception
  {
    public Error() {}
    public Error(Throwable cause) {super(cause);}
  }

  // listener for various game events
  public static interface Listener
  {
    // new game started
    void onNewGameStarted();
    // on game resume started
    void onGameResumeStarted(Game g);
    // on resume
    void onGameResumeRequested(Game g);
    // on game resume finished
    void onGameResumeFinished(Game g);

    // on replay started
    void onReplayStarted(Game g);

    // after deal ended
    void onAfterDeal(Game g);
    // bidding solve
    void onBiddingStart(Game g);
    // player declares bid
    void onPlayerBid(Player p, BidData bid);
    // first human bid
    void onFirstHumanBid(Game game, Player human);
    // bidding ended with declarer
    void onBiddingEnd();
    // contracting ended
    void onContractingEnd();
    // talon revealed
    void onTalonShow(Game g, Settings settings);
    // talon taken
    void onTalonTaken(Game g, Settings settings);
    // player makes contract
    void onPlayerContract(Player p, BidData bid);
    // dropping start
    void onDroppingStart(Game g);
    // player drops 2 cards
    void onPlayerDrop(Game g, Settings settings, Player p);
    // opponent chooses open game
    void onOpenGameType(Game g, Settings settings, Player opponent1, Player opponent2);
    // opponent chooses standing game
    void onStandingGameType(Game g, int hand0, int hand1, int hand2, int trick) throws Error;
    // moving round starts
    void onMovingRoundStart();
    // make player move card
    void onPlayCard(Game g, Player player, Card card);
    // called before moving round ends
    void onBeforeMovingRoundEnd(Game g, Player mover);
    // called after moving round ends
    void onAfterMovingRoundEnd(Game game, Settings settings, Player winner);
    // called before calculating score
    void onBeforeScore(Game g);
    // talon card played
    void onTalonCardPlayed(Settings settings, int round);
    // show score
    void onScore(Game g);
    // set is over
    void onSetOver(Game g);
    // expose initial state
    void onShowInitialPosition(Game g, Settings settings);
    // game is over
    void onGameOver(Game g);
    // offer declined
    void onOfferDeclined();
    // open player's cards
    void onOpenPlayerCards(Game g, Settings settings, int id);

    // on 'tricking 10' check started
    void onTrickingTenCheckStarted(Game g);
    // check 'tricking 10' game
    void onTrickingTenCheckRequested(Game g);
    // on 'tricking 10' check started
    void onTrickingTenCheckFinished(Game g);

    // on game resume started
    void onFastMovingStarted(Game g);
    // on fast moving
    void onFastMovingRequested(Game g);
    // on game resume finished
    void onFastMovingFinished(Game g);

    // resolver events
    void onResolverInitialize(Game g, int evaluator, int declarer, int starter, int trump, int hand0, int hand1, int hand2, byte talon0, byte talon1, int dropReal, int dropGuessed, boolean open) throws Error;
    // initial tree resolving
    void onBestScoreRequested(Game g) throws Error;
    // initial tree resolving
    void onSolveRequested(Game g, Player mover) throws Error;
    // advance resolver
    void onResolverAdvance(Game g, Settings settings, int move) throws Error;

    // replay start
    void onReplayStart();
    // replay end
    void onReplayEnd();
  }

  // game type in "moving" mode
  public static enum Type {UNKNOWN, STANDING, OPEN}

  public static class HandData
  {
    public int initial;
    public int current;
    public boolean open;
    public final ArrayList<Card.Suit> suits = new ArrayList<Card.Suit>(Card.Suit.rising.length);

    private void reset(boolean open)
    {
      // do not clean initial, it is required to be left intact in the "Begin" state
      //initial = 0;
      current = 0;
      this.open = open;
      suits.clear();
    }
  }

  // bid container
  public static class BidData
  {
    public Bid current;
    public Bid max;
    // bid count
    public int count;
    public boolean visible;

    public void reset()
    {
      current = max = null;
      count = 0;
      visible = false;
    }

    @Override
    public String toString()
    {
      return current + "/" + max;
    }
  }

  public static class TrickData
  {
    // hash value of the previous trick
    public int last;
    // current trick
    public int current;

    public void reset()
    {
      last = 0;
      current = 0;
      list.clear();
    }

    public int trash(int trash)
    {
      // copy current trick data to the previous one
      last = current;
      // add trick to the list
      list.add(last);
      // trash current trick cards
      trash = Trick.trash(current, trash);
      // reset current trick
      current = 0;

      return trash;
    }

    public int tricks(int id, Card.Suit trump)
    {
      int tricks = 0;

      for (int hash : list)
        if (Trick.winner(hash, trump) == id)
          ++tricks;

      return tricks;
    }

    public final ArrayList<Integer> list() {return list;}

    // list of tricks in the game
    private final ArrayList<Integer> list = new ArrayList<Integer>(Game.HAND_SIZE);
  }

  public static final class ServiceData
  {
    public long memory;
    public int nodes;
    public int moves;
  }

  // must be static, otherwise GSON deserializes it incorrect (this equals to null!)
  public static final class PlayerData
  {
    // game dealer
    public int dealer = Game.NO_ID;
    // starting player and hand (player can play other player's hand)
    public int starting = Game.NO_ID;
    // highest bidder - determined in contract mode (or null if going to play passing)
    public int declarer = Game.NO_ID;
    // player who plays against the declarer (the main whister)
    public int opponent = Game.NO_ID;
    // player who plays against the declarer (the passer)
    public int passer = Game.NO_ID;
    // player declared half whist
    public int half = Game.NO_ID;
    // current player and hand (player can play other player's hand)
    public int current = Game.NO_ID;

    // cards in player possession (hash for each suit)
    public HandData[] hands = new HandData[PLAYERS];
    // last and max bids - becomes contract after contracting mode
    public BidData[] bids = new BidData[PLAYERS];
    // player scores
    private Score[] scores = new Score[PLAYERS];
    // tricks taken so far in the current set
    public int[] tricks = new int[PLAYERS];

    public PlayerData()
    {
      // set the first dealer to be random
      dealer = Deck.instance().getRandomDealer();

      for (int i = 0; i < PLAYERS; ++i)
      {
        hands[i] = new HandData();
        bids[i] = new BidData();
        scores[i] = new Score();
      }
    }

    public void reset(int id, boolean open)
    {
      // clear cards
      hands[id].reset(open);
      // clear bids
      bids[id].reset();
      // clear tricks count
      tricks[id] = 0;
    }
  }

  // create player data object
  public final PlayerData player = new PlayerData();

  // tricks (previous and current) used in card modes (misere, passing, tricking)
  public TrickData trick = new TrickData();
  // latest bid
  public Bid bid;
  // current trick - used in card modes (misere, passing, tricking)
  //public Trick trick = new Trick();
  // current hand index - this is how players know what hand are they when it's time to make a move
  public int hand;
  // rounds played in the set - used in card modes
  public int rounds;
  // number of passers in the current round - used in bidding mode
  public int passers;
  // number of whisters in the current round - used in contracting mode
  public int whisters;
  // number of sets played in the game
  public int sets;
  // consequent passing sets
  public int consequentPassingSets;
  // cards that are out of the game
  public int trash;
  // current game type
  public Type type;
  // current state
  public String state;
  // current mode
  public String mode;
  // input options
  public Input.Options options = new Input.Options();
  // is it replay
  public boolean replay;
  // game's statistics
  public Statistics statistics = new Statistics();
  // should 'tricking 10' game be checked by human
  public boolean check10;
  // input
  public Input input = new Input();
  // system log
  public List<String> log = new LinkedList<String>();

  // service data
  public transient ServiceData service = new ServiceData();

  public Game(Settings settings)
  {
    this();

    // initialize player mountains using Rostov's rules
    if (settings.isRostov())
      for (Player player : players())
        getScore(player.id()).mountain.add(settings.poolSize / 2);

    // new game always starts from dealing mode
    set(Mode.dealing);
  }

  public void reset()
  {
    // clear input options
    options.clear();
    // clear input
    input.reset();

    // clear drop
    for (int i = 0; i < Game.TALON_SIZE; ++i)
      setDrop(i, 0);

    // reset trick
    trick.reset();

    // reset players
    for (Player p : players)
      p.reset(this);

    // clear declarer from previous set
    player.declarer = player.opponent = player.passer = player.half = player.current = Game.NO_ID;
    // clear game standing/open mode
    type = Game.Type.UNKNOWN;
    // reset number of passers
    passers = 0;
    // clear highest bid
    bid = null;
    // clear check 10 flag
    check10 = false;
    // syslog
    log.clear();
  }

  public Game clone()
  {
    return gson.fromJson(gson.toJson(this), this.getClass());
  }

  public boolean isOpen() {return type == Type.OPEN;}

  // get talon
  public int[] talon() {return talon;}
  // set talon's card
  public void setTalon(int i, int hash) {talon[i] = hash;}
  // get talon's card hash
  public int getTalonHash(int i) {return talon[i];}
  // get talon's card compressed
  public byte getTalonCompressed(int i) {return Card.card(talon[i]).compressed;}

  // get drop
  public int[] drop() {return drop;}
  // get drop's card hash
  public int getDropHash(int i) {return drop[i];}
  // set drop's card
  public void setDrop(int i, int hash) {drop[i] = hash;}

  // get human player (the one with id 0)
  public final Player human() {return player(0);}

  // score game
  public void score() {set(State.score);}

  // end the game
  public void end() {set(State.showPool1);}

  // change current state
  public void set(State state)
  {
    // save the real game state in the "resume" state
    if (state == State.resume)
      state.next(state());

    // set temporary game state to "resume"
    this.state = state.getClass().getName();
  }

  // get current state
  public State state() {return State.get(state);}

  // change current mode
  public void set(Mode mode)
  {
    this.mode = mode.getClass().getName();
    // new mode requires changing state to Initializing
    set(State.initialize);
  }

  // get current mode
  public Mode mode() {return Mode.get(mode);}

  // get current trump
  public Card.Suit trump() {return player.declarer == Game.NO_ID ? Card.Suit.NONE : getDeclarerBid().current.trump;}

  // get all players
  public final Player[] players() {return players;}
  // get player by id
  public Player player(int id) {return NO_ID < id && id < Game.PLAYERS ? players[id] : null;}

  // get next player (on the left)
  public Player next(Player player) {return this.player(next(player.id()));}
  public static int next(int player) {return (player + 1) % PLAYERS;}
  // get previous player (on the right)
  public Player previous(Player player) {return this.player(previous(player.id()));}
  public static int previous(int player) {return --player < 0 ? PLAYERS - 1 : player;}

  // make a move
  public void move(int move, Player player)
  {
    Card c = Card.card(move);

    Log.debug(Log.TAG, player + " plays " + c);

    trick.current = Trick.add(trick.current, player.id(), c.compressed);

    // remove card from player's hand
    HandData hand = this.player.hands[player.id()];
    hand.current = Hand.remove(hand.current, move);
  }

  public Player dealer() {return player(player.dealer);}
  public Player declarer() {return player(player.declarer);}
  public Player opponent() {return player(player.opponent);}
  public Player passer() {return player(player.passer);}
  public Player half() {return player(player.half);}
  public Player starting() {return player(player.starting);}
  public Player current() {return player(player.current);}

  // get declarer player hand
  public HandData getDeclarerHand() {return player.hands[player.declarer];}
  // get human player hand
  public HandData getHumanHand() {return player.hands[0];}
  // get current player hand
  public HandData getCurrentPlayerHand() {return player.hands[player.current];}
  // get opponent player hand
  public HandData getOpponentHand() {return player.hands[player.opponent];}
  // get passer player hand
  public HandData getPasserHand() {return player.hands[player.passer];}
  // get player hand
  public HandData getHand(int id) {return player.hands[id];}

  // get declarer player bid
  public BidData getDeclarerBid() {return getBid(player.declarer);}
  // get human player bid
  public BidData getHumanBid() {return getBid(0);}
  // get current player bid
  public BidData getCurrentPlayerBid() {return getBid(player.current);}
  // get half-whist player bid
  public BidData getHalfBid() {return getBid(player.half);}
  // get passer player bid
  public BidData getPasserBid() {return getBid(player.passer);}
  // get current player bid
  public BidData getBid(int id) {return player.bids[id];}
  // set player bids
  public void setBid(int id, Bid current, Bid max) {setCurrentBid(id, current); setMaxBid(id, max);}
  // set player current bid
  public void setCurrentBid(int id, Bid bid) {player.bids[id].current = bid;}
  // set player max bid
  public void setMaxBid(int id, Bid bid) {player.bids[id].max = bid;}

  // get declarer player number of tricks
  public int getDeclarerTricks() {return getTricks(player.declarer);}
  // get opponent player number tricks
  public int getOpponentTricks() {return getTricks(player.opponent);}
  // get passer player number tricks
  public int getPasserTricks() {return getTricks(player.passer);}
  // get player number of tricks
  public int getTricks(int id) {return player.tricks[id];}
  // set tricks of player
  public void setTricks(int id, int tricks) {player.tricks[id] = tricks;}
  // update tricks of player by provided delta
  public void addTricks(int id, int tricks) {player.tricks[id] += tricks;}

  // get human player score
  public Score getHumanScore() {return getScore(0);}
  // get player score
  public Score getScore(int id) {return player.scores[id];}
  // get contract
  public int contract() {return getDeclarerBid().current.tricks;}

  // test if half whist might be declared
  public boolean canHalfWhist() {return contract() == 6 || getDeclarerBid().current.tricks == 7;}

  public int getStartingTricks(Settings settings)
  {
    switch (settings.passingExit)
    {
      case PE_6666:
        return 6;

      case PE_6777:
        return consequentPassingSets == 0 ? 6 : 7;

      case PE_6788:
        return consequentPassingSets == 0 ? 6 : (consequentPassingSets == 1 ? 7 : 8);

      case PE_678678:
        int mod = consequentPassingSets % 3;
        return mod == 0 ? 6 : (mod == 1 ? 7 : 8);
    }

    // should never reach
    return 0;
  }

  private Game()
  {
    // populate players array except for the dummy player
    // create human players
    players[0] = new Human(this, 0, "Me");
    // create AI players
    // todo: create strategies depends on opponent levels settings
    players[1] = new AI(this, 1, "O1", Strategy.SIMPLE);
    players[2] = new AI(this, 2, "O2", Strategy.SIMPLE);
  }

  // players
  private transient Player[] players = new Player[PLAYERS];
  // talon cards hashes
  private int[] talon = new int[Game.TALON_SIZE];
  // drop cards hashes
  private int[] drop = new int[Game.TALON_SIZE];

/*
  public static void main(String[] argv)
  {
    Game g = new Game();

    String s = Game.gson.toJson(g);
    System.out.println(s);

    g = Game.gson.fromJson(s, Game.class);
    System.out.println(g.players + ", " + g.service);
  }
*/
}
